package com.otom.bcel;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.exception.MethodInvocationException;

public class MappingInfoGenerator {
	private static final String MAPPING_CODE_VM = "mapping_code.vm";
	private static final String VELOCITY_PROPERTIES = "velocity.properties";
	
	static {
		try {
			Properties properties = new Properties();
			properties.load(MappingInfoGenerator.class.getClassLoader().getResourceAsStream(
					VELOCITY_PROPERTIES));
			Velocity.init(properties);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void generate(Class<?>... classList) throws Exception {
		try {
			ClassMapping classMapping = new ClassMapping();
			classMapping.initFromAnnotations(classList);
			//System.out.println(classMapping);

			generateMappingCode(classMapping);
		} catch (MethodInvocationException e) {
			if (e.getCause() instanceof ValidationException) {
				throw (ValidationException)e.getCause();
			} else {
				throw e;
			}
			
		}
	}

	private void generateMappingCode(ClassMapping classMapping)
			throws Exception, NotFoundException, IOException,
			CannotCompileException {		
		ClassPool classPool = ClassPool.getDefault();

		// inialise classes
		for (ClassInfo classInfo : classMapping.getClassInfoList()) {
			CtClass ctClass = classPool.get(classInfo.getSource().getName());

			boolean found = false;
			for (CtClass iFaces : ctClass.getInterfaces()) {
				if (iFaces.getName().equals("com.otom.bcel.IMappable")) {
					found = true;
				}
			}

			for (CtMethod ctMethod : ctClass.getMethods()) {
				if (ctMethod.getDeclaringClass() ==  ctClass) {
					if (ctMethod.getName().equals("getMapper")) {
						ctClass.removeMethod(ctMethod);
					}
				}
			}

			if (!found) {
				CtClass imappable = classPool.get(IMappable.class.getName());
				ctClass.addInterface(imappable);
			}
			
			String mapperClassStr = classInfo.getSource().getName() + classInfo.getDestination().getSimpleName() + "Mapper";
			CtClass mapperClass = null;
			try {
				mapperClass = classPool.get(mapperClassStr);

				for (CtMethod ctMethod : mapperClass.getMethods()) {
					if (ctMethod.getName().equals("map")) {
						ctClass.removeMethod(ctMethod);
					}
				}
			} catch (NotFoundException nfe) {
				mapperClass = classPool.makeClass(mapperClassStr);
				CtClass imapperIface = classPool.get(IMapper.class.getName());
				mapperClass.addInterface(imapperIface);
			}			
		}
		
		Set<CtClass> ctClassSet = new HashSet<CtClass>();
		for (ClassInfo classInfo : classMapping.getClassInfoList()) {
			CtClass ctClass = classPool.get(classInfo.getSource().getName());
			String mapperClassStr = classInfo.getSource().getName() + classInfo.getDestination().getSimpleName() + "Mapper";
			CtClass mapperClass = classPool.get(mapperClassStr);

			CtClass objectClass = classPool.get("java.lang.Object");
			CtMethod mapMethod = new CtMethod(CtClass.voidType, "map",
					new CtClass[] { objectClass, objectClass }, mapperClass);

			VelocityContext context = new VelocityContext();
			context.put("classInfo", classInfo);

			Template template = Velocity.getTemplate(MAPPING_CODE_VM);
			StringWriter writer = new StringWriter();
			template.merge(context, writer);

			writer.flush();
			writer.close();

			String code = writer.toString();
			System.out.println("=======================================================================");
			System.out.println("Dumping generated Code : " + mapperClassStr);
			System.out.println(code);
			System.out.println("=======================================================================");
			mapMethod.setBody("{" + code + "}");

			mapperClass.addMethod(mapMethod);
			mapperClass.writeFile();
			
			CtMethod getMapperMethod = null;
			for (CtMethod ctMethod : ctClass.getMethods()) {
				if (ctMethod.getDeclaringClass() ==  ctClass) {
					if (ctMethod.getName().equals("getMapper")) {
						getMapperMethod = ctMethod;
						break;
					}
				}
			}
			if (getMapperMethod != null) {
				getMapperMethod.insertBefore("{" + 
						"if ($1 == " + classInfo.getDestination().getName() +
						".class)return new " + mapperClass.getName() + "();" +
						"}");
			}
			else {
				getMapperMethod = new CtMethod(classPool.get(IMapper.class
						.getName()), "getMapper", new CtClass[] { classPool
						.get(Class.class.getName()) }, ctClass);
				
				getMapperMethod.setBody("{if ($1 == " + classInfo.getDestination().getName() +
						".class)return new " + mapperClass.getName() + "();return null;}");
				ctClass.addMethod(getMapperMethod);
			}	
			ctClassSet.add(ctClass);
		}
		
		for (CtClass ctClass2 : ctClassSet) {
			ctClass2.writeFile();
		}
	}
	
	public static void main(String... args) {
		try {
			List<Class<?>> classes = new ArrayList<Class<?>>();
			
			for (String arg : args) {
				classes.add(Class.forName(arg));
			}
			
			if (classes.size() > 0) {
				MappingInfoGenerator mappingInfoGenerator = new MappingInfoGenerator();
				mappingInfoGenerator.generate(classes.toArray(new Class<?>[0]));
			}
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}
}
